package educrypt.cryptanalysis.playfair;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import educrypt.analysis.Frequencies;
import educrypt.analysis.FrequencyAnalysis;
import educrypt.ciphers.playfair.Playfair;
import educrypt.commons.Alphabet;
import educrypt.commons.Text;
import educrypt.commons.util.TextUtil;
import educrypt.database.dao.FrequencyAnalysisDAO;
import educrypt.exceptions.EduCryptException;
import educrypt.i18n.Messages;
import educrypt.log.ObservableLogger;
import educrypt.log.SimpleLogger;

public class CryptAnalysisPlayfair {
	private Text text;
	private Alphabet alphabet;

	private int NGRAM_SIZE = 2;

	private Frequencies averageFrequencies;

	public CryptAnalysisPlayfair(Text cipherText, Alphabet alphabet) {
		final int TEXT_MAX_LENGTH = 5000;
		String content = cipherText.getContent();
		if (content.length() > TEXT_MAX_LENGTH) {
			content = content.substring(0, TEXT_MAX_LENGTH);
		}
		this.text = new Text(cipherText.getName(), cipherText.getType(),
				cipherText.getLanguage(), content);
		this.alphabet = alphabet;
	}

	private void logGeneration(int genNumber, List<Key> candidates) {
		SimpleLogger logger = ObservableLogger.getInstance();
		logger.log(SimpleLogger.TITLE, Messages.getString("guiGeneration"));
		logger.log(SimpleLogger.CONTENT, String.valueOf(genNumber));
		logger.log(SimpleLogger.TITLE, Messages.getString("guiCandidates"));
		logger.log(SimpleLogger.CONTENT, String.valueOf(candidates));
		logger.log("");
	}

	public String cryptAnalyze(int maxCandidates, int survivorsPercentage,
			int maxGenerations) throws EduCryptException {
		SimpleLogger logger = ObservableLogger.getInstance();
		logger.log(SimpleLogger.TITLE,
				"---- " + Messages.getString("guiPlayfairEvolutionary")
						+ " ----");

		logger.log(SimpleLogger.TITLE, Messages.getString("guiMaxCandidates"));
		logger.log(SimpleLogger.CONTENT, String.valueOf(maxCandidates));

		logger.log(SimpleLogger.TITLE, Messages.getString("guiMaxGenerations"));
		logger.log(SimpleLogger.CONTENT, String.valueOf(maxGenerations));

		logger.log(SimpleLogger.TITLE, Messages.getString("guiMaxSurvivors"));
		logger.log(SimpleLogger.CONTENT, String.valueOf(survivorsPercentage));
		logger.log("");

		int realSurvivors = Math.round((survivorsPercentage / 100f)
				* maxCandidates);

		List<Key> candidateKeys = new ArrayList<Key>(maxCandidates);

		long startTime = System.currentTimeMillis();

		// Obtenemos las frecuencias medias (por pares)
		FrequencyAnalysisDAO analysisDAO = FrequencyAnalysisDAO.getInstance();

		averageFrequencies = analysisDAO.getAverageFrequencies(
				this.alphabet.getName(), NGRAM_SIZE, text.getLanguage(),
				text.getType());

		// Generamos las primeras claves
		candidateKeys = generateRandomKeys(maxCandidates);
		// Evaluamos los candidatos
		candidateKeys = evaluate(candidateKeys); // Realmente no hace falta la
													// asignación
		// Ordenamos los candidatos
		Collections.sort(candidateKeys, new KeyByFitness());

		// Mostramos info
		logGeneration(0, candidateKeys);
		// System.out.println("Generation 0");
		// System.out.println(candidateKeys);
		// System.out.println("Time(seg): "
		// + ((System.currentTimeMillis() - startTime) / 1000));

		for (int i = 0; i < maxGenerations; i++) {
			// long generationTime = System.currentTimeMillis();

			// Eliminamos los menos aptos
			candidateKeys = candidateKeys.subList(0, realSurvivors);

			// Reproduccón o mutación
			List<Key> newCandidates;
			if (Math.random() > 0.5) {
				if (Math.random() > 0.1) {
					newCandidates = evaluate(mutation2(candidateKeys));
				} else {
					newCandidates = evaluate(mutation(candidateKeys));
				}
			} else {
				if (Math.random() > 0.1) {
					newCandidates = evaluate(crossover2(candidateKeys));
				} else {
					newCandidates = evaluate(crossover(candidateKeys));
				}
			}

			// Añadimos crossovers y mutaciones
			newCandidates.removeAll(candidateKeys);// candidateKeys.removeAll(newCandidates);
			candidateKeys.addAll(newCandidates);

			// Generamos claves nuevas hasta tener maxCandidates
			int numberOfNewKeys = maxCandidates - candidateKeys.size();
			if (numberOfNewKeys > 0) {
				candidateKeys
						.addAll(evaluate(generateRandomKeys(numberOfNewKeys)));
			}
			// Ordenamos los candidatos
			Collections.sort(candidateKeys, new KeyByFitness());

			// Mostramos info
			logGeneration(i + 1, candidateKeys);
			// System.out.println("Generation " + (i + 1));
			// System.out.println(candidateKeys);
			// System.out.println("Time(seg): "
			// + ((System.currentTimeMillis() - generationTime) / 1000));
		}

		System.out.println("Total Time(min): "
				+ ((System.currentTimeMillis() - startTime) / 60000));
		return candidateKeys.get(0).symbols;
	}

	private List<Key> evaluate(List<Key> keysToEvaluate) {
		for (Key candidate : keysToEvaluate) {
			candidate.fitness = evaluateKey(candidate.symbols);
		}
		return keysToEvaluate;
	}

	private List<Key> mutation(List<Key> toMutate) {
		List<Key> mutatedKeys = new ArrayList<Key>(toMutate.size());
		for (int i = 0; i < toMutate.size(); i++) {
			String key = toMutate.get(i).symbols;
			int randPicker1 = (int) (Math.random() * (key.length() - 1));
			int randPicker2 = (int) (Math.random() * (key.length() - 1));

			mutatedKeys.add(new Key(replacePos(randPicker1, randPicker2, key)));
		}
		return mutatedKeys;
	}

	private List<Key> mutation2(List<Key> toMutate) {
		List<Key> mutatedKeys = new ArrayList<Key>(toMutate.size());
		for (int i = 0; i < toMutate.size(); i++) {
			String key = toMutate.get(i).symbols;
			int randPicker1 = (int) (Math.random() * (key.length() - 1));
			StringBuffer newKey = new StringBuffer(key.substring(randPicker1))
					.append(key.substring(0, randPicker1));

			mutatedKeys.add(new Key(newKey.toString()));
		}
		return mutatedKeys;
	}

	private static String replacePos(int pos1, int pos2, String st) {
		char[] data = st.toCharArray();
		char aux = data[pos1];
		data[pos1] = data[pos2];
		data[pos2] = aux;
		return String.valueOf(data);
	}

	private List<Key> crossover2(List<Key> toCrossOver) {
		List<Key> crossoverResult = new ArrayList<Key>();
		for (int i = 0; i < toCrossOver.size(); i++) {
			for (int j = i + 1; j < toCrossOver.size(); j++) {
				String key1 = toCrossOver.get(i).symbols;
				String key2 = toCrossOver.get(j).symbols;
				int randPicker = (int) (Math.random() * (key1.length() - 1));

				String newKey1 = key1.substring(0, randPicker)
						+ key2.substring(randPicker, key2.length());
				String newKey2 = key2.substring(0, randPicker)
						+ key1.substring(randPicker, key2.length());

				Key k1 = new Key(alphabet.getCopyBeginningWith(
						TextUtil.removeRepeatedSymbols(newKey1)).getSymbols());
				Key k2 = new Key(alphabet.getCopyBeginningWith(
						TextUtil.removeRepeatedSymbols(newKey2)).getSymbols());

				crossoverResult.add(k1);
				crossoverResult.add(k2);
			}
		}
		return crossoverResult;
	}

	private List<Key> crossover(List<Key> toCrossOver) {

		List<Key> crossoverResult = new ArrayList<Key>();
		for (int i = 0; i < toCrossOver.size(); i++) {
			for (int j = i + 1; j < toCrossOver.size(); j++) {
				String key1 = toCrossOver.get(i).symbols;
				String key2 = toCrossOver.get(j).symbols;
				int randPicker = (int) (Math.random() * (key1.length() - 1));
				String[] newKeys = replacePos(randPicker, key1, key2);
				Key k1 = new Key(newKeys[0]);
				Key k2 = new Key(newKeys[1]);
				if (k1.equals(k2)) {
					List<Key> aux = new ArrayList<Key>();
					aux.add(k2);
					k2 = mutation2(aux).get(0);
				}
				crossoverResult.add(k1);
				crossoverResult.add(k2);
			}
		}
		return crossoverResult;
	}

	private static String[] replacePos(int pos, String st1, String st2) {
		char charAtSt1 = st1.charAt(pos);
		char charAtSt2 = st2.charAt(pos);

		// Buscar la posiciones de esos caracteres en las otras cadenas
		int posAtSt1 = st1.indexOf(charAtSt2);
		int posAtSt2 = st2.indexOf(charAtSt1);

		String[] result = new String[2];
		result[0] = replacePos(pos, posAtSt1, st1);
		result[1] = replacePos(pos, posAtSt2, st2);
		return result;

	}

	private List<Key> generateRandomKeys(int maxCandidates) {
		List<Key> generatedKeys = new ArrayList<Key>(maxCandidates);
		String alphabetSymbols = alphabet.getSymbols();
		for (int i = 0; i < maxCandidates; i++) {
			generatedKeys.add(new Key(randomizeString(alphabetSymbols)));
		}
		return generatedKeys;
	}

	private static String randomizeString(String st) {
		StringBuffer randomizedString = new StringBuffer();
		List<Character> in = new ArrayList<Character>(st.length());
		for (char c : st.toCharArray()) {
			in.add(c);
		}

		while (in.size() > 0) {
			int randPicker = (int) (Math.random() * in.size());
			randomizedString.append(in.remove(randPicker));
		}
		return randomizedString.toString();
	}

	private double evaluateKey(String key) {
		// long startEval = System.currentTimeMillis();
		// PlayfairKeyTable keyTable = new PlayfairKeyTable(key, this.alphabet);

		// long startDec = System.currentTimeMillis();
		Playfair playfair = new Playfair(this.alphabet, key, false);
		Text decodedText = playfair.decode(this.text);
		// System.out.println("\t\tDecoding Time(ms) "
		// + (System.currentTimeMillis() - startDec));

		// long startAna = System.currentTimeMillis();
		// Frecuencia del texto decodificado
		FrequencyAnalysis frequencyAnalysis = new FrequencyAnalysis(
				decodedText, this.alphabet, NGRAM_SIZE);
		Frequencies decodedTextFrequencies = frequencyAnalysis.getFrequencies(); // No
		// necesito
		// ordenarlo
		// puesto que
		// estan en el mismo orden

		// System.out.println("\t\tAnalysis Time(ms) "
		// + (System.currentTimeMillis() - startAna));

		Double[] decodedTextFreqValues = decodedTextFrequencies.getValues();
		Double[] averageFreqValues = averageFrequencies.getValues();
		double sum = 0;
		for (int i = 0; i < decodedTextFreqValues.length; i++) {
			sum += Math.abs(averageFreqValues[i] - decodedTextFreqValues[i]);
		}

		// System.out.println("\tEvaluation Time(ms) "
		// + (System.currentTimeMillis() - startEval));
		return sum;
	}
}

class Key {
	String symbols;
	Double fitness;

	Key(String key) {
		this.symbols = key;
		fitness = 0d;
	}

	@Override
	public boolean equals(Object o) {
		return o instanceof Key && ((Key) o).symbols.equals(this.symbols);
	}

	@Override
	public int hashCode() {
		return symbols.hashCode();
	}

	@Override
	public String toString() {
		return symbols + " | " + fitness;
	}
}

class KeyByFitness implements Comparator<Key> {
	@Override
	public int compare(Key o1, Key o2) {
		if (o1.fitness < o2.fitness) {
			return -1;
		} else if (o1.fitness > o2.fitness) {
			return 1;
		} else {
			return 0;
		}
	}

}
